﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Utilities;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Build.Evaluation;
using System.Security;

namespace XNACompilator
{
    /// <summary>
    /// kind of plateform where ContentBuilder can compile
    /// </summary>
    public enum PlateformType
    {
        WINDOWS = 0,
        XBOX,
        WINDOWSPHONE,
        ZUNE,
    }

    /// <summary>
    /// Builder use to compile all asset and class into XNB
    /// </summary>
    public class ContentBuilder : IDisposable
    {
        #region CONST

        private const string XNAVERSION = ", Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, Culture=neutral, processorArchitecture=MSIL";

        private static readonly string[] PLATEFORM =
        {
            "Windows",
            "Xbox",
            "WindowsPhone",
            "Zune",
        };

        private static readonly string[] S_PIPELINEASSEMBLIES =
        {
            "Microsoft.Xna.Framework.Content.Pipeline.AudioImporters" + XNAVERSION,
            "Microsoft.Xna.Framework.Content.Pipeline.EffectImporter" + XNAVERSION,
            "Microsoft.Xna.Framework.Content.Pipeline.FBXImporter" + XNAVERSION,
            "Microsoft.Xna.Framework.Content.Pipeline.TextureImporter" + XNAVERSION,
            "Microsoft.Xna.Framework.Content.Pipeline.VideoImporters" + XNAVERSION,
            "Microsoft.Xna.Framework.Content.Pipeline.XImporter" + XNAVERSION,
        };

        #endregion

        #region STATIC

        private static int S_DIRECTORYSALT;

        #endregion

        #region Field

        private ErrorLogger m_error;
        private Microsoft.Build.Evaluation.Project m_proj;

        private string m_buildDrirectory;
        private string m_processDirectory;
        private string m_baseDirectory;
        private string m_outputdir;

        private bool m_isDisposed;

        #endregion

        #region ReadOnly

        private readonly string rd_platform;

        public static readonly string[] IMPORTERS;

        public static readonly string[] EXPORTER;

        #endregion

        #region CTOR

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="outputPath">Path to copy all the asset after compilation</param>
        /// <param name="type">Specifie the compile's platform</param>
        public ContentBuilder(string outputPath = "", PlateformType type = PlateformType.WINDOWS)
        {
            rd_platform = PLATEFORM[(int)type];

            m_outputdir = outputPath;

            CreateTempDirectory();
            CreateBuildProject();
        }

        #endregion

        #region Property
        /// <summary>
        /// Gets the output directory, which will contain the generated .xnb files.
        /// </summary>
        public string OutputDirectory
        {
            get { return Path.Combine(m_buildDrirectory, @"bin\Content"); }
        }

        #endregion

        #region Initialize

        /// <summary>
        /// Finalizes the content builder.
        /// </summary>
        ~ContentBuilder()
        {
            Dispose(false);
        }


        /// <summary>
        /// Disposes the content builder when it is no longer required.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }


        /// <summary>
        /// Implements the standard .NET IDisposable pattern.
        /// </summary>
        protected virtual void Dispose(bool disposing)
        {
            if (!m_isDisposed)
            {
                m_isDisposed = true;

                DeleteTempDirectory();
            }
        }

        #endregion

        #region MSBuild

        /// <summary>
        /// Creates a temporary MSBuild content project in memory.
        /// </summary>
        private void CreateBuildProject()
        {
            string projectPath = Path.Combine(m_buildDrirectory, "content.contentproj");
            string outputPath = Path.Combine(m_buildDrirectory, "bin");


            // Hook up our custom error logger.
            m_error = new ErrorLogger();

            // Create the build project.
            m_proj = new Microsoft.Build.Evaluation.Project();

            m_proj.FullPath = projectPath;

            m_proj.SetProperty("XnaPlatform", rd_platform);
            m_proj.SetProperty("XnaFrameworkVersion", "v4.0");
            m_proj.SetProperty("Configuration", "Release");
            m_proj.SetProperty("OutputType", "Library");
            m_proj.SetProperty("OutputPath", outputPath);
            m_proj.SetProperty("XNAContentPipelineTargetProfile", "Reach");

            // Register any custom importers or processors.
            foreach (string pipelineAssembly in S_PIPELINEASSEMBLIES)
            {
                AddReference(pipelineAssembly);
                //m_proj.AddItem("Reference", pipelineAssembly);
            }

            // Include the standard targets file that defines
            // how to build XNA Framework content.
            m_proj.Xml.AddImport("$(MSBuildExtensionsPath)\\Microsoft\\XNA " +
                                        "Game Studio\\v4.0\\Microsoft.Xna.GameStudio" +
                                        ".ContentPipeline.targets");
        }

        /// <summary>
        /// Adds a new content file to the MSBuild project. The importer and
        /// processor are optional: if you leave the importer null, it will
        /// be autodetected based on the file extension, and if you leave the
        /// processor null, data will be passed through without any processing.
        /// </summary>
        public void Add(Asset asset)
        {
            ProjectItem buildItem = m_proj.AddItem("Compile", asset.FileName).Last();

            buildItem.SetMetadataValue("Link", Path.GetFileName(asset.FileName));
            buildItem.SetMetadataValue("Name", asset.Name);

            if (asset.Importer != null)
            {
                buildItem.SetMetadataValue("Importer", asset.Importer.Name);
                foreach (var elem in asset.Importer.Parameters)
                    buildItem.SetMetadataValue("ImporterParameters_" + elem.Key, elem.Value);
            }

            if (asset.Exporter != null)
            {
                buildItem.SetMetadataValue("Processor", asset.Exporter.Name);
                foreach (var elem in asset.Exporter.Parameters)
                    buildItem.SetMetadataValue("ProcessorParameters_" + elem.Key, elem.Value);
            }
        }

        /// <summary>
        /// Method used to add a reference to permit a compilation its nessesary to gave the path of the DLL
        /// or is full name if it's an GAC's DLL
        /// </summary>
        /// <param name="referenceName">Path or the DLL or fullname in the GAC</param>
        public void AddReference(string referenceName)
        {
            var elem = m_proj.AddItem("Reference", referenceName);
            if (elem != null)
            {
                var item = elem.Last();
                item.SetMetadataValue("Private", "False");
            }
            m_proj.ReevaluateIfNecessary();
        }

        /// <summary>
        /// Removes all content files from the MSBuild project.
        /// </summary>
        public void Clear()
        {
            var elem = (from el in m_proj.Items
                        where el.ItemType == "Compile"
                        select el);
            m_proj.RemoveItems(elem);

            //m_proj.("Compile");
        }

        /// <summary>
        /// Builds all the content files which have been added to the project,
        /// dynamically creating .xnb files in the OutputDirectory.
        /// Returns an error message if the build fails.
        /// </summary>
        public string Build()
        {
            // Clear any previous errors.
            m_error.Errors.Clear();

            m_proj.ReevaluateIfNecessary();

            // Build the project.
            if (!m_proj.Build(m_error))
            {
                // If the build failed, return an error string.
                return string.Join("\n", m_error.Errors.ToArray());
            }

            Clear();

            return null;
        }

        #endregion

        #region Temp Directories

        /// <summary>
        /// Creates a temporary directory in which to build content.
        /// </summary>
        private void CreateTempDirectory()
        {
            // Start with a standard base name:
            //
            //  %temp%\WinFormsContentLoading.ContentBuilder


            m_baseDirectory = Path.Combine(Path.GetTempPath(), GetType().FullName);

            // Include our process ID, in case there is more than
            // one copy of the program running at the same time:
            //
            //  %temp%\WinFormsContentLoading.ContentBuilder\<ProcessId>

            int processId = Process.GetCurrentProcess().Id;

            m_processDirectory = Path.Combine(m_baseDirectory, processId.ToString());

            // Include a salt value, in case the program
            // creates more than one ContentBuilder instance:
            //
            //  %temp%\WinFormsContentLoading.ContentBuilder\<ProcessId>\<Salt>

            S_DIRECTORYSALT++;

            m_buildDrirectory = Path.Combine(m_processDirectory, S_DIRECTORYSALT.ToString());

            // Create our temporary directory.
            Directory.CreateDirectory(m_buildDrirectory);

            PurgeStaleTempDirectories();
        }

        /// <summary>
        /// Deletes our temporary directory when we are finished with it.
        /// </summary>
        private void DeleteTempDirectory()
        {
            Directory.Delete(m_buildDrirectory, true);

            // If there are no other instances of ContentBuilder still using their
            // own temp directories, we can delete the process directory as well.
            if (Directory.GetDirectories(m_processDirectory).Length == 0)
            {
                Directory.Delete(m_processDirectory);

                // If there are no other copies of the program still using their
                // own temp directories, we can delete the base directory as well.
                if (Directory.GetDirectories(m_baseDirectory).Length == 0)
                {
                    Directory.Delete(m_baseDirectory);
                }
            }
        }

        /// <summary>
        /// Ideally, we want to delete our temp directory when we are finished using
        /// it. The DeleteTempDirectory method (called by whichever happens first out
        /// of Dispose or our finalizer) does exactly that. Trouble is, sometimes
        /// these cleanup methods may never execute. For instance if the program
        /// crashes, or is halted using the debugger, we never get a chance to do
        /// our deleting. The next time we start up, this method checks for any temp
        /// directories that were left over by previous runs which failed to shut
        /// down cleanly. This makes sure these orphaned directories will not just
        /// be left lying around forever.
        /// </summary>
        private void PurgeStaleTempDirectories()
        {
            // Check all subdirectories of our base location.
            foreach (string directory in Directory.GetDirectories(m_baseDirectory))
            {
                // The subdirectory name is the ID of the process which created it.
                int processId;

                if (int.TryParse(Path.GetFileName(directory), out processId))
                {
                    try
                    {
                        // Is the creator process still running?
                        Process.GetProcessById(processId);
                    }
                    catch (ArgumentException)
                    {
                        // If the process is gone, we can delete its temp directory.
                        Directory.Delete(directory, true);
                    }
                }
            }
        }

        #endregion
    }
}
